package mt.network;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;

import mt.Order;
import mt.comm.ClientMessage;
import mt.comm.ClientSideMessage;
import mt.comm.ServerComm;
import mt.comm.ServerSideMessage;

/**
 * Communication class for a MicroTrader server. 
 * The MicroTrader server must communicate with clients through this class.
 * 
 * @author Grupo45
 * 
 */
public class ServerCommNT implements ServerComm {

	public static final int PORT = 45720;

	private LinkedBlockingQueue<ServerSideMessage> receivedMessages = new LinkedBlockingQueue<ServerSideMessage>();
	private ConcurrentHashMap<String, LinkedBlockingQueue<ClientSideMessage>> clients = new ConcurrentHashMap<String, LinkedBlockingQueue<ClientSideMessage>>();
	private ConnectionController connectionController = new ConnectionController();

	@Override
	public void start() {
		System.out.println("start");
		(new Thread(connectionController)).start();

	}

	@Override
	public synchronized ServerSideMessage getNextMessage() {
		try {
			while (receivedMessages.isEmpty()) {
				wait();
			}
			return receivedMessages.remove();
		} catch (InterruptedException e) {
			return null;
		}
	}

	@Override
	public boolean hasNextMessage() {
		return !receivedMessages.isEmpty();
	}

	@Override
	public void sendOrder(String toNickname, Order order) {
		sendMessage(toNickname, new ClientMessage().setOrder(order));
	}

	@Override
	public void sendError(String toNickname, String error) {
		sendMessage(toNickname, new ClientMessage().setError(error));
	}

	private void sendMessage(String toNickname, ClientSideMessage msg) {
		if (clientIsConnected(toNickname)) {
			clients.get(toNickname).add(msg);
		}
	}

	@Override
	public boolean clientIsConnected(String nickname) {
		return clients.containsKey(nickname);
	}

	@Override
	public void disconnectClient(String nickname) {
		if (clientIsConnected(nickname)) {
			clients.get(nickname);
			clients.remove(nickname);
		}
	}

	private ClientSideMessage parseConnect(ServerSideMessage msg) {
		if (!clientIsConnected(msg.getSenderNickname())) {
			clients.put(msg.getSenderNickname(),
					new LinkedBlockingQueue<ClientSideMessage>());
			addReceivedMessage(msg);
			return new ClientMessage();
		} else {
			if (clients.get(msg.getSenderNickname()).isEmpty()) {
				return new ClientMessage();
			} else {
				return clients.get(msg.getSenderNickname()).remove();
			}
		}
	}

	private ClientSideMessage parseNewOrder(ServerSideMessage msg) {
		
		if (clientIsConnected(msg.getSenderNickname())) {
			addReceivedMessage(msg);
			return new ClientMessage();
		} else {
			return new ClientMessage().setError("Not Connected");
		}

	}

	private ClientSideMessage parseDisconnect(ServerSideMessage msg) {
		if (clientIsConnected(msg.getSenderNickname())) {
			clients.get(msg.getSenderNickname()).clear();
			disconnectClient(msg.getSenderNickname());
			addReceivedMessage(msg);
			return new ClientMessage();
		} else {
			return new ClientMessage().setError("Not Connected");
		}
	}

	private synchronized void addReceivedMessage(ServerSideMessage msg) {
		receivedMessages.add(msg);
		notifyAll();
	}

	public synchronized void stop() {
		connectionController.isStopped = true;

		connectionController.stop();
	}

	private class ConnectionController implements Runnable {
		private ServerSocket serverSocket = null;
		private boolean isStopped = false;
		private Thread runningThread = null;

		public void run() {
			synchronized (this) {
				this.runningThread = Thread.currentThread();
			}
			openServerSocket();
			while (!isStopped()) {
				Socket clientSocket = null;
				try {
					clientSocket = this.serverSocket.accept();
				} catch (IOException e) {
					if (isStopped()) {
						System.out.println("Server Stopped.");
						return;
					}
					throw new RuntimeException(
							"Error accepting client connection", e);
				}
				new Thread(new Connection(clientSocket)).start();
			}
			System.out.println("Server Stopped.");
		}

		private synchronized boolean isStopped() {
			return this.isStopped;
		}

		public synchronized void stop() {
			this.isStopped = true;
			try {
				this.serverSocket.close();
			} catch (IOException e) {
				throw new RuntimeException("Error closing server", e);
			}
		}

		private void openServerSocket() {
			try {
				this.serverSocket = new ServerSocket(PORT);
			} catch (IOException e) {
				throw new RuntimeException("Cannot open port " + PORT, e);
			}
		}
	}

	private class Connection implements Runnable {

		private final Socket clientSocket;

		public Connection(Socket clientSocket) {
			this.clientSocket = clientSocket;
		}

		public void run() {
			try {
				ObjectInputStream input = new ObjectInputStream(
						clientSocket.getInputStream());
				ObjectOutputStream output = new ObjectOutputStream(
						clientSocket.getOutputStream());
				
				ServerSideMessage msg = (ServerSideMessage) input.readObject();
				ClientSideMessage reply;
				switch (msg.getType()) {
				case CONNECTED:
					reply = parseConnect(msg);
					break;
				case DISCONNECTED:
					reply = parseDisconnect(msg);
					break;
				case NEW_ORDER:
					reply = parseNewOrder(msg);
					break;
				default:
					reply = new ClientMessage()
							.setError("Invalid message type");
					break;
				}
				output.writeObject(reply);
				output.flush();

				input.close();
				output.close();
			} catch (ClassNotFoundException e) {
				throw new RuntimeException("Invalid class");
			} catch (IOException e1) {
			}finally{
				try {
					clientSocket.close();
				} catch (IOException e) {
					throw new RuntimeException("fail closing socket");
				}
			}
		}

	}



}